# 面经手册 · 第38篇《MyBatis 一对一、一对多怎么查?延迟加载原理和 N+1 问题怎么解?》
作者:小傅哥
博客:https://bugstack.cn (opens new window)
沉淀、分享、成长,让自己和他人都能有所收获!😄
# 一、前言
实际业务中,数据表之间的关系几乎不可能是一张表独立存在的。用户有订单、订单有商品、商品有分类……关联查询是日常开发的高频操作。
MyBatis 中 association 和 collection 是处理关联查询的两大标签,延迟加载是解决性能问题的关键配置,N+1 问题更是面试必问的经典坑。
# 二、面试题
谢飞机,小记!,面试继续。
面试官:MyBatis 一对一关联查询怎么配置?
谢飞机:用 association 标签。
面试官:嵌套查询和嵌套结果有什么区别?
谢飞机:嵌套查询是两条 SQL,嵌套结果是一条 SQL?
面试官:对,那嵌套查询有什么性能问题?
谢飞机:N+1?
面试官:怎么解决 N+1?
谢飞机:用延迟加载?
面试官:延迟加载原理是什么?
谢飞机:代理?动态代理?
面试官:什么代理?JDK 还是 CGLIB?代理对象是怎么创建的?
谢飞机:我……再见!ヾ( ̄▽ ̄)
# 三、一对一关联 — association
# 1. 数据模型
user(用户表) id_card(身份证表)
┌──────┐ ┌──────────────┐
│ id │ │ id │
│ name │ ──── 1:1 ─│ user_id │
│ age │ │ card_no │
│ │ │ address │
└──────┘ └──────────────┘
2
3
4
5
6
7
# 2. 嵌套结果(推荐,一条 SQL)
<resultMap id="userWithCardMap" type="com.example.entity.User">
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="age" property="age"/>
<!-- 一对一关联 -->
<association property="idCard" javaType="com.example.entity.IdCard">
<id column="card_id" property="id"/>
<result column="card_no" property="cardNo"/>
<result column="card_address" property="address"/>
</association>
</resultMap>
<select id="findUserWithCard" resultMap="userWithCardMap">
SELECT
u.id, u.name, u.age,
c.id AS card_id, c.card_no, c.address AS card_address
FROM user u
LEFT JOIN id_card c ON u.id = c.user_id
WHERE u.id = #{id}
</select>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 3. 嵌套查询(两条 SQL,需延迟加载配合)
<resultMap id="userLazyMap" type="com.example.entity.User">
<id column="id" property="id"/>
<result column="name" property="name"/>
<!-- 延迟加载关联查询 -->
<association property="idCard"
javaType="com.example.entity.IdCard"
select="com.example.mapper.IdCardMapper.findByUserId"
column="id"
fetchType="lazy"/>
</resultMap>
<select id="findById" resultMap="userLazyMap">
SELECT id, name, age FROM user WHERE id = #{id}
</select>
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- IdCardMapper.xml -->
<select id="findByUserId" resultType="com.example.entity.IdCard">
SELECT id, card_no, address FROM id_card WHERE user_id = #{userId}
</select>
2
3
4
# 四、一对多关联 — collection
# 1. 数据模型
user(用户表) orders(订单表)
┌──────┐ ┌──────────────┐
│ id │ │ id │
│ name │ ── 1:N ── │ user_id │
│ age │ │ order_no │
│ │ │ amount │
└──────┘ └──────────────┘
2
3
4
5
6
7
# 2. 嵌套结果(一条 SQL)
<resultMap id="userWithOrdersMap" type="com.example.entity.User">
<id column="id" property="id"/>
<result column="name" property="name"/>
<!-- 一对多关联 -->
<collection property="orders" ofType="com.example.entity.Order">
<id column="order_id" property="id"/>
<result column="order_no" property="orderNo"/>
<result column="amount" property="amount"/>
</collection>
</resultMap>
<select id="findUserWithOrders" resultMap="userWithOrdersMap">
SELECT
u.id, u.name,
o.id AS order_id, o.order_no, o.amount
FROM user u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.id = #{id}
</select>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
注意:ofType 指定集合中元素的类型,不是 javaType。
# 3. 嵌套查询(两条 SQL)
<resultMap id="userLazyOrdersMap" type="com.example.entity.User">
<id column="id" property="id"/>
<result column="name" property="name"/>
<collection property="orders"
ofType="com.example.entity.Order"
select="com.example.mapper.OrderMapper.findByUserId"
column="id"
fetchType="lazy"/>
</resultMap>
2
3
4
5
6
7
8
9
# 五、延迟加载原理
# 1. 配置方式
<!-- mybatis-config.xml -->
<settings>
<!-- 全局延迟加载开关 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 按需加载(false=访问关联属性时才加载,true=加载主对象后立即加载所有关联) -->
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
2
3
4
5
6
7
# 2. 局部配置覆盖全局
<!-- 强制立即加载 -->
<association property="idCard" fetchType="eager" .../>
<!-- 强制延迟加载(即使全局关闭了延迟加载) -->
<association property="idCard" fetchType="lazy" .../>
2
3
4
5
# 3. 底层原理:代理对象
延迟加载的核心:返回的不是目标对象本身,而是一个代理对象。
正常加载(eager):
查 user → 执行 SQL → 获取 user 对象(真实对象)
延迟加载(lazy):
查 user → 执行 SQL → 获取 user 代理对象
↓ 访问 user.getIdCard() 时
触发代理拦截 → 执行关联 SQL → 获取 idCard → 注入到 user 对象
↓ 再次访问
直接返回已加载的 idCard(不再执行 SQL)
2
3
4
5
6
7
8
9
# 4. 源码追踪
代理创建:
// org.apache.ibatis.executor.resultset.DefaultResultSetHandler
private Object createResultObject(ResultSetWrapper rsw,
ResultMap resultMap, ...) {
// 检查是否需要延迟加载
if (hasNestedResultMaps(rsw, resultMap)) {
// 创建代理对象
return createProxyObject(resultMap, ...);
}
return objectFactory.create(resultType);
}
// 代理工厂(CGLIB 或 Javassist)
private Object createProxyObject(ResultMap resultMap, ...) {
return proxyFactory.createProxy(resultObject,
new ResultLoaderMap(), lazyLoader, configuration, ...);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
代理拦截:
// org.apache.ibatis.executor.loader.javassist.JavassistProxyFactory
public class JavassistProxyFactory implements ProxyFactory {
@Override
public Object createProxy(Object target, ...) {
return EnhancedResultObjectProxyImpl.createProxy(target, ...);
}
}
// 代理拦截逻辑
public static class EnhancedResultObjectProxyImpl implements MethodHandler {
@Override
public Object invoke(Object enhanced, Method method,
Method proxyMethod, Object[] args) {
String methodName = method.getName();
Object value;
// 检查是否是关联属性的 getter 方法
if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) {
// 触发延迟加载
value = lazyLoader.load(finalizeMethod);
}
// 加载完成后调用原始方法
return proxyMethod.invoke(target, args);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 5. 代理实现选择
<!-- mybatis-config.xml -->
<settings>
<!-- CGLIB 或 JAVASSIST -->
<setting name="proxyFactory" value="JAVASSIST"/>
</settings>
2
3
4
5
| 实现 | 特点 |
|---|---|
| JAVASSIST(默认) | 字节码生成,启动快,运行稍慢 |
| CGLIB | 字节码生成,功能强大,需要额外依赖 |
# 六、N+1 问题
# 1. 什么是 N+1
场景:查询 10 个用户,每个用户关联一张身份证
嵌套查询模式(N+1):
第 1 条 SQL:SELECT * FROM user(查出 10 个用户)
第 2 条 SQL:SELECT * FROM id_card WHERE user_id = 1
第 3 条 SQL:SELECT * FROM id_card WHERE user_id = 2
...
第 11 条 SQL:SELECT * FROM id_card WHERE user_id = 10
总共 11 条 SQL = 1 + 10 = N+1
2
3
4
5
6
7
8
9
10
# 2. 解决方案
方案一:嵌套结果(JOIN)
<!-- 一条 SQL 搞定,推荐 -->
<select id="findUserWithCard" resultMap="userWithCardMap">
SELECT u.*, c.* FROM user u
LEFT JOIN id_card c ON u.id = c.user_id
</select>
2
3
4
5
方案二:延迟加载
<!-- 开启延迟加载后,只有真正访问关联属性时才执行 SQL -->
<setting name="lazyLoadingEnabled" value="true"/>
2
方案三:批量查询(fetchSize)
<association property="idCard"
select="com.example.mapper.IdCardMapper.batchFindByUserIds"
column="id"
fetchType="lazy"/>
2
3
4
<!-- 使用 IN 批量查询 -->
<select id="batchFindByUserIds" resultType="com.example.entity.IdCard">
SELECT * FROM id_card WHERE user_id IN
<foreach collection="list" item="userId" open="(" separator="," close=")">
#{userId}
</foreach>
</select>
2
3
4
5
6
7
方案四:二级缓存
<!-- 第一次查询后缓存,后续命中缓存 -->
<cache/>
2
# 3. 各方案对比
| 方案 | SQL 数量 | 适用场景 | 注意事项 |
|---|---|---|---|
| JOIN 嵌套结果 | 1 条 | 关联数据确定需要 | JOIN 数据量大时性能下降 |
| 延迟加载 | 按需 | 关联数据不一定用到 | 仍然可能 N+1 |
| 批量 fetchSize | 1 + 1 = 2 | 嵌套查询 | 需配置 batch |
| 二级缓存 | 首次 N+1,后续 0 | 数据变更少 | 缓存一致性问题 |
# 七、常见面试追问
# Q1:association 和 collection 能嵌套使用吗?
能。可以在 collection 中嵌套 association,实现三层甚至更深的关联。但层级越深,SQL 复杂度和性能影响越大。
# Q2:延迟加载对序列化有影响吗?
有。代理对象序列化时可能丢失关联数据。解决:在序列化前先访问关联属性触发加载,或配置 serialization 代理工厂。
# Q3:fetchType 优先级?
局部 fetchType 优先级高于全局 lazyLoadingEnabled 配置。fetchType="eager" 强制立即加载,fetchType="lazy" 强制延迟加载。
# 八、总结
记住三个核心要点:
1. 关联查询两种方式
嵌套结果(JOIN 一条 SQL):推荐,性能好
嵌套查询(两条 SQL):灵活,但可能 N+1
2. 延迟加载原理
返回代理对象 → 访问关联属性时拦截 → 执行关联 SQL
通过 Javassist/CGLIB 创建代理,invoke() 触发加载
3. N+1 问题解决
优先用 JOIN 嵌套结果(1条SQL)
或延迟加载 + 批量 fetchSize(2条SQL)
二级缓存可作为辅助
2
3
4
5
6
7
8
9
10
11
12
13
14
面试回答模板:
MyBatis 通过 association 和 collection 标签处理一对一和一对多关联查询。两种实现方式:嵌套结果用 JOIN 一条 SQL 查完,推荐使用;嵌套查询分两条 SQL 执行,灵活但可能产生 N+1 问题。
延迟加载的原理是返回代理对象而非真实对象,通过 Javassist 或 CGLIB 创建代理,拦截 getter 方法,在访问关联属性时才触发关联 SQL 执行。配置上全局开启 lazyLoadingEnabled=true,局部可通过 fetchType 覆盖。
N+1 问题的最优解是嵌套结果用 JOIN 查询,如果必须用嵌套查询,则配合延迟加载和批量 fetchSize 来减少 SQL 次数。

